package NIOCommunication;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Date;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Semaphore;

public class NIOSocketContext extends NIOContext
{
	protected SocketChannel mSocketChannel;
	protected ByteBuffer mReadBuffer;
	protected ByteBuffer mCurrentWriteBuffer;
	protected int mTimeout = 15000; // ms
	protected Date mTimeoutTime;
	protected int mBufferLength = 1024;//32 * 1024;
	protected Queue<byte[]> mQueuedWrites;
        protected boolean mClosed = false;
        public static int MaxOpenSockets = 200;
        public static int CurrentlyOpenSockets = 0;
        public boolean mTCPError = false;
	
	public NIOSocketContext(NIOProcessor processor, SocketChannel socketChannel)
	{
		super(processor);
		mReadBuffer = ByteBuffer.allocateDirect(mBufferLength);
		mQueuedWrites = new LinkedList<byte[]>();
		
		mSocketChannel = socketChannel;
        //System.out.println("Opened socket " + (++numSockets));
		try
		{
			mSocketChannel.configureBlocking(false);
			try
			{
				mNIOProcessor.RegisterContextOps(this, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
			}
			catch (ClosedChannelException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
static int numSockets = 0;
	public NIOSocketContext(NIOProcessor processor, InetSocketAddress address)
	{
		super(processor);
		mReadBuffer = ByteBuffer.allocateDirect(mBufferLength);
		mQueuedWrites = new LinkedList<byte[]>();
		
		try
		{
                        
                        mSocketChannel = SocketChannel.open();
                        ++CurrentlyOpenSockets;
                        
                        if(CurrentlyOpenSockets > MaxOpenSockets)
                        {
                            System.out.println("Too many open sockets! (" + CurrentlyOpenSockets + "/" + MaxOpenSockets + ")");
                        }
                        
                        mSocketChannel.configureBlocking(false);
			
			mSocketChannel.connect(address);
			mNIOProcessor.RegisterContextOps(this, SelectionKey.OP_CONNECT);
                        mTimeoutTime = new Date();
                        mTimeoutTime.setTime(mTimeoutTime.getTime() + mTimeout);
                        mNIOProcessor.SetConnectionTimeout(this);
                        //mNIOProcessor.QueueCommand(new NIOProcessorRegisterTimeoutCommand(this));
			
                        //System.out.println("Opened socket " + (++numSockets));
			//System.out.println("Waiting for connection...");
		}
		catch (IllegalStateException e)
		{
			System.out.println("Connection error!!!");
                        mTCPError = true;
			mNIOProcessor.QueueCommand(new NIOProcessorCloseContextCommand(this));
			e.printStackTrace();
		}
		catch (ClosedChannelException e)
		{
			// TODO Auto-generated catch block
			System.out.println("Connection error!!!");
                        mTCPError = true;
			mNIOProcessor.QueueCommand(new NIOProcessorCloseContextCommand(this));
			e.printStackTrace();
		}
		catch (IOException e)
		{
			// TODO Auto-generated catch block
			System.out.println("Connection error!!!");
                        mTCPError = true;
			mNIOProcessor.QueueCommand(new NIOProcessorCloseContextCommand(this));
			e.printStackTrace();
		}
	}
	
	public NIOSocketContext(NIOProcessor processor) throws UnknownHostException
	{
		this(processor, new InetSocketAddress(InetAddress.getLocalHost(), 9876));
	}
        
    public boolean IsTimeoutElapsed()
    {
    	return new Date().after(mTimeoutTime);
    }
	
	@Override
	public void OnConnectable(SelectionKey key)
	{
		//System.out.println("OnConnectable");
		
		// Connection OK
	    //System.out.println("Server Found");

	    // Close pendent connections
	    if (mSocketChannel.isConnectionPending())
	    {
	    	try
	    	{
				if(!mSocketChannel.finishConnect())
				{
					//System.out.println("Connection error!!!");
					//mNIOProcessor.QueueCommand(new NIOProcessorCloseContextCommand(this));
                                    
				}
				//mNIOProcessor.RegisterContextOps(this, SelectionKey.OP_READ);// | SelectionKey.OP_WRITE);
				mNIOProcessor.QueueCommand(new NIOProcessorUpdateOpsCommand(this, SelectionKey.OP_READ | SelectionKey.OP_WRITE));
				//System.out.println("Connected!");
                                //System.out.println("SocketChannel address: " + mSocketChannel.socket().getInetAddress());
			}
	    	catch (IOException e)
	    	{
				// TODO Auto-generated catch block
	    		System.out.println("Connection error!!! " + e.getMessage());
                        mTCPError = true;
				mNIOProcessor.QueueCommand(new NIOProcessorCloseContextCommand(this));
				//e.printStackTrace();
	    	}
                
                mNIOProcessor.ClearConnectionTimeout(this);
	    }
            else
            {
                    System.out.println("Connect without connectionPending???");
            }
	}
        
        public boolean IsWritePending()
        {
            return (mQueuedWrites.size() != 0) || (mCurrentWriteBuffer != null);
        }
        
	@Override
	public void OnReadable(SelectionKey key)
	{
		//System.out.println("Reading=========================================");
		try
		{
			int readLength = 0;
			if( (readLength = mSocketChannel.read(mReadBuffer)) != -1)
			{
				//System.out.println("read " + readLength + ". buf = " + mReadBuffer);
				mReadBuffer.flip();
				byte[] bytes = new byte[mReadBuffer.remaining()];
				mReadBuffer.get(bytes);
				mReadBuffer.clear();
				OnReadData(bytes);
                                //System.out.println("Bytes: " + new String(bytes));
			}
			else
			{
				//System.out.println("read end-of-file.  Requesting socket close.");
				mNIOProcessor.QueueCommand(new NIOProcessorCloseContextCommand(this));
			}
		}
		catch (IOException e)
		{
			// TODO Auto-generated catch block
			//e.printStackTrace();
			mNIOProcessor.QueueCommand(new NIOProcessorCloseContextCommand(this));
		}
	}
	
	@Override
	public void OnWritable(SelectionKey key)
	{
		//System.out.println("Writing=========================================");
		if(mQueuedWrites.size() == 0 && (mCurrentWriteBuffer == null || !mCurrentWriteBuffer.hasRemaining()))
		{
			CancelWriteOp();
			return;
		}
		
		if(mCurrentWriteBuffer == null)
		{
                    if(mQueuedWrites.size() == 0)
                    {
                        CancelWriteOp();
                        return;
                    }
			mCurrentWriteBuffer = ByteBuffer.wrap(mQueuedWrites.poll());
		}
		
		try
		{
			mSocketChannel.write(mCurrentWriteBuffer);
		}
		catch (IOException e)
		{
			//System.out.println("Write error");
			// TODO Auto-generated catch block
			e.printStackTrace();
			mNIOProcessor.QueueCommand(new NIOProcessorCloseContextCommand(this));
		}
		
		if(!mCurrentWriteBuffer.hasRemaining())
		{
			mCurrentWriteBuffer = null;
		}
	}
	
	public void OnReadData(byte[] data)
	{
		System.out.println("Text = \n" + new String(data));
		if(new String(data).contains("Reply please"))
		{
			WriteData("Here is your reply".getBytes());
		}
	}
	
	@Override
	public void Close()
	{
		try
		{
			if(mSocketChannel != null)
			{
				mSocketChannel.close();
				mSocketChannel = null;
                                CurrentlyOpenSockets--;
				//System.out.println("Closed Socket");
			}
			
			if(mReadBuffer != null)
			{
				mReadBuffer = null;
			}
			
			if(mCurrentWriteBuffer != null)
			{
				mCurrentWriteBuffer = null;
			}
                        mClosed = true;
                        OnChannelClosed(this);
			
            //System.out.println("Closed socket " + (numSockets--));
			
			//if(ConnectionTimer != null)
			//{
			//	ConnectionTimer.cancel();
			//	ConnectionTimer = null;
			//}
		}
		catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
        
        public static boolean IsSocketAvailable()
        {
            return CurrentlyOpenSockets < MaxOpenSockets;
        }
	
	public void RequestWriteOp()
	{
		mNIOProcessor.QueueCommand(new NIOProcessorUpdateOpsCommand(this, SelectionKey.OP_READ | SelectionKey.OP_WRITE));
	}
	
	public void CancelWriteOp()
	{
		mNIOProcessor.QueueCommand(new NIOProcessorUpdateOpsCommand(this, SelectionKey.OP_READ));	
	}
        
        public void OnChannelClosed(NIOSocketContext context)
        {
            
        }
	
	public void WriteData(byte[] data)
	{
		mQueuedWrites.add(data);
		if(mSocketChannel != null && mSocketChannel.isConnected())
		{
			RequestWriteOp();
		}
		//ByteBuffer buf = ByteBuffer.wrap(data);
		//int length = 0;
		//try
		//{
		//	while((length += mSocketChannel.write(buf)) < data.length);
		//}
		//catch (IOException e)
		//{
		//	// TODO Auto-generated catch block
		//	e.printStackTrace();
		//}
		//System.out.println("Wrote " + length + " bytes");
	}
	
	public void WriteTest(int i)
	{
		StringBuilder text = new StringBuilder();
		text.append(i);
		for(int j = 0; j < 12 + i; ++j)
		{
			text.append(text.toString());
		}
		text.append("\nDone test " + i);
		WriteData(text.toString().getBytes());
		//WriteData(("This is test number " + i + " and it is pretty long on purpose.\n\0").getBytes());
	}
        
        public boolean IsConnected()
        {
            if(mSocketChannel == null)
            {
                return false;
            }
            
            return mSocketChannel.isConnected();
        }
        
        public boolean IsClosed()
        {
            if(mSocketChannel == null)
            {
                return true;
            }
            
            return mClosed;
        }

	@Override
	public SelectableChannel GetChannel()
	{
		return mSocketChannel;
	}
}
